iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 19
1

寫在前面

雖然C/C++非常相近,但如果要在兩者中間拉一條線
那中間一定可以放下golang跟rust
這兩個經常被互相比較的語言其實各自走了C跟C++的道路

golang走向了C的簡潔,C只有32個保留字,而golang只有25個
Rust走向了C++的安全,基本上只要編譯得過的程式,基本上他就可以安然無恙的執行到程式結束

因此在C/C++分別寫出linux跟windows後,Rust也寫出了自己的作業系統Redox

https://ithelp.ithome.com.tw/upload/images/20200920/20127836swfJeilruh.png

雖然是0.5版,但是基本功能已經都有了,
左上角是網頁瀏覽器,右上角是計算機,
左下角是檔案管理器,右下角是終端機,
中間則是廣告文字編輯器

目前官方不建議使用虛擬機跑(但還是可以這麼做)
如果你想要體驗一下系統,而你手邊又有linux的話可以這麼做

就像golang有個強勁的老爹Google一樣,Rust的爸爸是Mozilla,也就是跟Chrome齊名的瀏覽器Firefox的老爹
所以不必擔心有一天你的語言不更新了

另外Rust也有類似node的npm可以用,也就是cargo(我們之後馬上就會使用到他)

先去找安裝或找docker來使用吧
準備好了我們就開始

cargo

寫給docker使用者

使用docker的人記得先掛載你的工作資料夾

docker container run --rm -it -v "$PWD":/home/rust rust:slim

$PWD是linux的當前目錄指令,如果你不是linux或是終端機沒辦法使用PWD指令,記得把他換成你的工作目錄

建立使用者

Rust的管理工具cargo必須要有使用者才能使用
所以如果你是使用docker來建立rust環境的話需要先建立使用者

先建立使用者 ubuntu (你可以自己替換成你喜歡的名字)

root@9bd0708b24b5:/# useradd ubuntu  

接著給這個使用者最高權限(root)

root@9bd0708b24b5:/# usermod -g root ubuntu

接著切換使用者

root@9bd0708b24b5:/# su ubuntu

建立專案

然後你就可以用cargo來建立你的專案了

cargo new convert

convert是專案名稱,你可以替換成任何你想要的

如果你出現

error: Failed to create package `convert` at `/home/rust/convert`

Caused by:
  failed to create directory `/home/rust/convert`

Caused by:
  Permission denied (os error 13)

代表你的掛載的工作資料夾可能是root權限或是唯讀權限,去修正一下即可

如果成功的話裡面應該會出現一些檔案
使用docker的人因為你已經掛載了資料夾所以你可以在你的本地看產生的目錄了

convert
├── Cargo.toml
└── src
    └── main.rs

Cargo.toml是cargo的專案設定檔
src裡頭的main.rs則是程式真正執行的位置
專案資料夾甚至已經準備好git可以使用了

接下來我們可以來寫程式了

程式

在你的本機將下面的程式碼填進main.rs這個檔案

use std::io;

fn main() {
    println!("Tell me what you want to do:");
    println!("(1)T to H (2)H to T");
    let mut input = String::new();
    io::stdin().read_line(&mut input).expect("Read fail");

    let inputstr = input.as_str();
    match inputstr {  //用input.trim()也可以
        "1\n" =>{  //用input.trim()就直接用"1"
            println!("Please enter the number:");
            let mut number= String::new();
            io::stdin().read_line(&mut number).expect("Read fail");
            println!("{}",t2h(number));
        },
        "2\n" =>{
            println!("Please enter the number:");
            let mut number= String::new();
            io::stdin().read_line(&mut number).expect("Read fail");
            println!("{}",h2t(number));
        },
        _ =>{
            println!("Wrong selection");
        }
    }
}

fn h2t(input: String) -> i64{
    let mut output:i64 = 0;
    let mut i:usize = 0;
    while i < input.len()-1{
        let order:u32 = (input.len()-i-2)as u32;
        output += ae_return(&input[i..i+1])*i64::pow(16,order);
        i+=1;
    }
    return output;
}

fn ae_return(number:&str)->i64{
    match number{
        "A" => return 10 ,
        "B" => return 11 ,
        "C" => return 12 ,
        "D" => return 13 ,
        "E" => return 14 ,
        "F" => return 15 ,
        _ => return number.parse().unwrap_or(0)
    }
}


fn t2h(input: String)-> String{
    let num = input.trim().parse().unwrap_or(0);

    for i in 0..16 {
        for j in 0..16 {
            for k in 0..16 {
                if i64::pow(16, 2)*i+i64::pow(16,1)*j+i64::pow(16,0)*k == num{
                    return format!("{}{}{}",return_ae(i),return_ae(j),return_ae(k));     
                }
            }
        }
    }
    return "".to_string();
}

fn return_ae(input: i64) -> String{
    if input<10{
        return input.to_string();
    }else{
        match input{
            10 => return "A".to_string() ,
            11 => return "B".to_string() ,
            12 => return "C".to_string() ,
            13 => return "D".to_string() ,
            14 => return "E".to_string() ,
            15 => return "F".to_string() ,
            _ => return "".to_string()
        }
    }
}

接下來在docker或本地端進入產生的專案目錄後執行cargo run就可以看到你的程式跑起來了

$ cargo new convert
     Created binary (application) `convert` package
$ ls
convert
$ cd convert
$ cargo run
   Compiling convert v0.1.0 (/home/rust/convert)
    Finished dev [unoptimized + debuginfo] target(s) in 1.44s
     Running `target/debug/convert`
Tell me what you want to do:
(1)T to H (2)H to T

順便介紹一下幾個cargo的編譯命令

編譯並執行
cargo run
編譯不執行
cargo build
檢查不編譯
cargo check

結構

由於rust是編譯式語言,因此跟golang以及C#等等相同有著程式進入點

fn main(){}

而需要引用的函式庫則會在程式的開頭引用

use std::io;

用法跟C++類似

行尾則需要加上;

輸入及輸出

我們往下看到main方法吧

println!("Tell me what you want to do:");
println!("(1)T to H (2)H to T");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Read fail");

println!是rust輸出的語法,如果不想換行可以使用print!

let就是Rust宣告變數的方式,作用類似C#的var,會自動判斷型別
但是因為Rust還是靜態型別的語言,所以右邊還是需要告訴Rust你想要丟入的值的型別
這裡我們使用

String::new();

::表示使用在String這個命名空間內的new()方法
這個方法將給我們一個String的物件,我們將他丟在input這個變數裡

mut表示這個變數是可變的
在Rust裡頭若是沒有這個關鍵字,變數一旦宣告將會無法改變他的值
你可以嘗試將mut拿掉,變成

let input = String::new();
io::stdin().read_line(&input).expect("Read fail");

並使用cargo check幫你檢查看看是否編譯會過
應該會回你

 --> src/main.rs:7:27
  |
7 |     io::stdin().read_line(&input).expect("Read fail");
  |                           ^^^^^^ types differ in mutability
  |
  = note: expected mutable reference `&mut std::string::String`
                     found reference `&std::string::String`

看到Rust編譯器的厲害了嗎?
他甚至會告訴你哪裡錯了以及該怎麼改

看看下一行

io::stdin().read_line(&mut input).expect("Read fail");

這個是Rust讀取輸入的方式
注意這裡的io是因為開頭我們引用了函式庫

use std::io

所以可以直接使用io,否則我們這一行需要改成

std::io::stdin().read_line(&mut input).expect("Read fail");

後面的expect則是錯誤接回的方式
功能類似golang的err
Rust類似的功能還有幾種,不過我們先介紹這個方式

型別

Rust有個有趣的地方是字串有兩種
第一種是我們原本宣告的String,這裡是指長度可變的字串
第二種是&str,這裡是指固定長度的字串
由於我們不知道使用者輸入的長度會有多少,所以使用String
但是在進行值比對時比對的值必定是固定長度的因此會是&str型別
而Rust是強型別語言因此當兩者的型別不同因此無法比對
所以我們需要轉型,將String這個型別轉型成&str

let inputstr = input.as_str();

如果你需要轉型回去可以使用

let inputstr = inputstr.to_string();

或是使用String提供的方法

input.trim()

如同我在註解裡面使用的那樣

轉型完之後才能丟到match裡面進行比對

邏輯處理

在Rust裡面,switch的格式為

match something{
    something =>{

    },
    something =>{

    },
    _ => {

    }
}

因此這裡我們會這樣寫

match inputstr {  //用input.trim()也可以
    "1\n" =>{  //用input.trim()就直接用"1"
        println!("Please enter the number:");
        let mut number= String::new();
        io::stdin().read_line(&mut number).expect("Read fail");
        println!("{}",t2h(number));
    },
    "2\n" =>{
        println!("Please enter the number:");
        let mut number= String::new();
        io::stdin().read_line(&mut number).expect("Read fail");
        println!("{}",h2t(number));
    },
    _ =>{
        println!("Wrong selection");
    }
}

如果你的指令不需要多行也可以這樣使用

match number{
    "A" => return 10 ,
    "B" => return 11 ,
    "C" => return 12 ,
    "D" => return 13 ,
    "E" => return 14 ,
    "F" => return 15 ,
    _ => return number.parse().unwrap_or(0)
}

_ 就是類似以前的default,當沒有任何可匹配時將會引導至這裡

這裡要注意兩點,
第一,不需要case跟break
第二,每個條件需要使用,分開

之後每個case的內容都跟以往一樣再次詢問使用者問題,
印出時使用的方法是我們之前介紹過得println!

println!("{}",t2h(number));

這時裡面有個{}
這是因為輸出時預設只能印出字串,因此非字串的型別可以使用填充字元幫助格式化
功能類似C#的

Console.WriteLine("{0} {1} {2}",returnAE(i),returnAE(j),returnAE(k));

如果你需要其他種格式化可以用下面的方式

// 指定位置格式化
println!("here is {1}, here is {0}", "first", "second");
// 指定名稱格式化
println!("{first} {second} {third}",
    first="I am first",
    second="here is second one",
    third="this is 3rd"
);

Rust的println!可以玩的方式還有很多種,這裡先介紹到這邊

方法的結構

Rust是靜態型別語言,因此宣告方法時必須同時宣告輸入與輸出的型別

Rust的方法結構長這樣

fn 方法名稱(變數名稱: 變數型別) -> 回傳型別{

}

因此沒有輸入值及回傳值的main方法長這樣

fn main(){

}

而h2t這個將十六進位轉成十進位的方法需要輸入為String且輸出為數值就這樣寫

fn h2t(input: String) -> i64{

}

i64就如同我們以前寫的float64,是一個具有64bits儲存空間的浮點數

裡頭的第一行

let mut output:i64 = 0;

這裡是另外一種變數的宣告方式,我們宣告變數值順便給值,並且告訴他變數的型別
第二行的

let mut i:usize = 0;

usize則會根據系統去做判定,當作業系統是64位元時,型別為u64
當作業系統為32位元時,型別為u32
這樣可以確保作業系統有足夠的容量還保留空間給變數

迴圈while

Rust的while迴圈同樣不需要()來包住條件
裡頭我們使用

i < input.len()-1

會需要-1是因為我們剛才輸入時的換行符號被涵蓋在字串長度內了
因此你也可以改用trim()來確保輸入值的正確性

i < input.trim().len()

這樣也能取到正確的長度而沒有換行符

往下看到while裡頭的內容

let order:u32 = (input.len()-i-2)as u32;

這裡會將計算值給轉型是因為在冪次計算pow中,裡面的型別必須為u32
(我們後面真的會介紹各種數值型別啦,這裡先知道就好)
這樣下面那一行才會成立

output += ae_return(&input[i..i+1])*i64::pow(16,order);

pow是由i64這個命名空間給予的方法,因此前面需要i64::

而這一行裡頭將字串的字元取出的方式為

input[i..i+1]

指的是將陣列的值從i取到i+1前,例如

input[0..1]

就會取出陣列內第0個

至於沒辦法像以往這樣

input[i]

寫的原因是因為Rust的字串在取字元時沒辦法輸入usize的型別
(但若是陣列就可以,參考這篇)

如果你需要直接取值可能需要轉型,參考這篇

match

我們往下看ae_return這個方法裡面的內容吧

fn ae_return(number:&str)->i64{
    match number{
        "A" => return 10 ,
        "B" => return 11 ,
        "C" => return 12 ,
        "D" => return 13 ,
        "E" => return 14 ,
        "F" => return 15 ,
        _ => return number.parse().unwrap_or(0)
    }
}

輸入值為&str型別,這樣我們就可以直接拿去跟"A"做比較了

還記得我們剛才說的match結構嗎?
如果你要執行的程式只有一行,那你就不需要使用{},但還是要,做條件的區隔

_ => 裡頭的

number.parse()

則是將&str轉型成i64的方法
後面的

unwrap_or(0)

則是錯誤接回的方法(畢竟字串轉型i64是有可能錯誤的)
裡面的0則是錯誤代碼

你可以將t2h的回傳錯誤值修改成這樣

fn t2h(input: String)-> String{
    let num = input.trim().parse().unwrap_or(8);

然後故意在輸入十進位轉十六進位時給錯誤的數值,他將會給你8號錯誤代碼
像這樣

Tell me what you want to do:
(1)T to H (2)H to T
1
Please enter the number:
ASDF //這裡故意輸入錯誤
008

得到8號

迴圈for

我們往下看到t2h的方法吧

let num = input.trim().parse().unwrap_or(0);

for i in 0..16 {
    for j in 0..16 {
        for k in 0..16 {
            if i64::pow(16, 2)*i+i64::pow(16,1)*j+i64::pow(16,0)*k == num{
                return format!("{}{}{}",return_ae(i),return_ae(j),return_ae(k));     
            }
        }
    }
}

方法一開始我們先將輸入的String給trim掉,確保轉型成i64時會正確

Rust的for迴圈跟Ruby一樣有

for i in 0..16 {}

可以用
條件內一樣不需要(),但是要{}

邏輯判斷if

if一樣不需要(),但需要{}

if i64::pow(16, 2)*i+i64::pow(16,1)*j+i64::pow(16,0)*k == num{
    return format!("{}{}{}",return_ae(i),return_ae(j),return_ae(k));     
}

前面說過pow是由i64提供的方法,因此需要使用i64::

return時的

format!("{}{}{}",return_ae(i),return_ae(j),return_ae(k));

則是一種格式化的方式

用法跟golang的

fmt.Sprintf("%s%s%s",returnAE(i),returnAE(j),returnAE(k))

類似,不印出東西,而是將內容格式化成字串
這邊格式化之後直接回傳

for迴圈之外的

return "".to_string()

則是因為有可能在迴圈(4095)內沒有匹配的值,我們在這裡回傳沒有內容物的字串
而不是空值

return_ae這個方法內的語法我們都介紹過了,就不詳細提拉

小結

我們來複習一下今天的語法吧

  • 基本結構
    • 編譯式語言,具有程式進入點
    • cargo會幫你製作最基本的格式
  • 印出/讀取
    • 使用println!輸出,可以使用"{}"來做格式化
    • io::stdin().read_line輸入
    • format!可以格式化輸出成字串(但是不會印出來)
  • 方法的結構
    • 關鍵字是fn
    • 靜態型別,方法必須提宣告回傳值的型別
  • 邏輯控制
    • match 不需要break跟(),條件用,做區隔
    • if 不需要()
  • 迴圈控制
    • for 用法類似Ruby的for
    • while 不需要(),但是需要{}
  • 型別
    • 記得String跟&str不一樣
    • usize可以依據作業系統去分配到底是u64還是u32
  • 冪次計算
    • 輸入值為u32型別
  • 基本上變數是不可變動的,除非使用關鍵字mut

Rust的編譯器是真的很強,錯誤時會告訴你錯誤的位置跟可能可以改進的方式
我在實作時幾乎不需要google就能一直寫下去
整體而言寫起來相當舒服
算是改進了C++遇到錯誤時不知道該怎麼解的缺點

不過若是已經習慣弱型別的工程師大概會寫得很不習慣,
因為光是字串就分成兩種,而且字串在使用上某些部份還跟陣列不一樣

當然,相對的也換來較為安全的程式碼
你甚至不用擔心執行時作業系統是64位元還是32位元,Rust會自動去判斷

明天我們再來介紹一款開頭也是R的語言
R

R什麼?

沒有,就R


上一篇
型別 沒有沒有型別的值
下一篇
R 所以我們當年到底為什麼要學 Matlab?
系列文
你會十五種程式語言?不,我會十五種HelloWorld.為了避免這種狀況,因此寫了這篇:淺入淺出十五種程式語言30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
skycover
iT邦新手 4 級 ‧ 2020-09-20 18:29:36

如果有任何寫不清楚或是觀念沒有很明白的話請留言告知我
會盡快補上

如果有任何寫錯的地方也麻煩留言告知我
會盡快修正

感謝各位

skycover iT邦新手 4 級 ‧ 2020-10-26 16:41:41 檢舉

感謝,已修正

0
DanSnow
iT邦好手 1 級 ‧ 2020-10-21 23:39:54

結構那邊第一行的 rust 拼錯了喔

我要留言

立即登入留言